package br.ufpb.di.ppgi.persistencia.pagination;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
 * Encapsula o resultado parcial de uma consulta, que retrata uma pagina especifica,
 * guardando todas as propriedades de paginação.
 * @author Tomaz Lavieri
 *
 * @param <T> O tipo de objeto que esta {@link PagedList} guarda
 */
public class PagedList<T> implements Serializable {
	private static final long serialVersionUID = -5977442952220466796L;
	private static int defaultMaxPages = 5;
	private final List<T> list;
	private final int numberOfResults;
	private final int maxResultOnPage;
	private final int page;
	private final int numberOfPages;
	private final int startedNumber;
	private final int finishedNumber;
	private final int listSize;
	private final List<Page> pages;
	private int maxPages = defaultMaxPages;
	private String base;
	private final String sortCriterion;
	private final String sortDirection;
	
	/**
	 * Quantidade padrão de páginas a serem exibidas no cabeçalho de paginação.
	 * @see #getMaxPages()
	 */
	public static int getDefaultMaxPages() {
		return defaultMaxPages;
	}
	/**
	 * Altera a quantidade padrão de páginas a serem exibidas no cabeçalho de paginação,
	 * esta alteração sera refletida em todas as proximas instancias criadas, alterando
	 * seus valores iniciais.<br/>
	 * <br/>
	 * Para alterações em uma instância especifica use {@link #setMaxPages(int)}
	 * @param defaultMaxPages a nova quantidade padrão
	 * @see #setMaxPages(int)
	 */
	public static void setDefaultMaxPages(int defaultMaxPages) {
		PagedList.defaultMaxPages = defaultMaxPages;
	}

	public PagedList(List<T> list,int numberOfResults,int maxResultOnPage) {
		this(list, numberOfResults, maxResultOnPage, null);
	}
	
	public PagedList(List<T> list,int numberOfResults,int maxResultOnPage,Integer page) {
		this(list,numberOfResults,maxResultOnPage,page,"","");
	}
	
	public PagedList(List<T> list, int numberOfResults, int maxResultOnPage, Integer page, String sortCriterion, String sortDirection) {
		this.list = list.size() > maxResultOnPage ? list.subList(0, maxResultOnPage) : list;
		this.numberOfResults = numberOfResults;
		this.maxResultOnPage = maxResultOnPage;
		this.page = page == null || page < 1 ? 1 : page;
		this.numberOfPages = (int)Math.ceil(numberOfResults/(double)maxResultOnPage);
		
		int lastOfLastPage = (getPage()-1)*getMaxResultOnPage();
		this.startedNumber = list.isEmpty() ? lastOfLastPage : lastOfLastPage + 1;
		this.finishedNumber = lastOfLastPage + list.size();
		this.listSize = list.size();
		this.pages = new ArrayList<Page>();
		this.sortCriterion = sortCriterion;
		this.sortDirection = sortDirection;
	}
	/**
	 * A lista de objetos <tt>T</tt> continadas nesta página.
	 */
	public List<T> getList() {
		return list;
	}
	/**
	 * O número de resultados totais da consultas, mostrando a quantidade
	 * contida em todas aspáginas.
	 */
	public int getNumberOfResults() {
		return numberOfResults;
	}
	/**
	 * A quantidade máxima de resultados exibidas em uma única página.
	 */
	public int getMaxResultOnPage() {
		return maxResultOnPage;
	}
	/**
	 * O número da página atual (a contagem de páginas inicia em 1)
	 */
	public int getPage() {
		return page;
	}
	/**
	 * O número de páginas totais da consulta.
	 */
	public int getNumberOfPages() {
		return numberOfPages;
	}
	/**
	 * O número do primeiro resultado desta página, portanto os resultados
	 * desta página vão de {@link #startedNumber} até {@link #finishedNumber}
	 */
	public int getStartedNumber() {
		return startedNumber;
	}
	/**
	 * O número do último resultado desta página, portanto os resultados
	 * desta página vão de {@link #startedNumber} até {@link #finishedNumber}
	 */
	public int getFinishedNumber() {
		return finishedNumber;
	}
	/**
	 * Informa se a página corrente é a última página da consulta. 
	 */
	public boolean isLastPage() {
		return page == numberOfPages;
	}
	/**
	 * Infroma se a página corrente é a primeira página da consulta.
	 */
	public boolean isFirstPage(){
		return page == 1;
	}
	/**
	 * Informa a quantidade de resultados exibidos nesta página.
	 */
	public int getListSize() {
		return listSize;
	}
	/**
	 * Informa se a página esta vazia. 
	 */
	public boolean isEmpty() {
		return listSize == 0;
	}
	/**
	 * Informa se a página corrente está fora do alcance para consulta, ou seja,
	 * verifica se a página é maior que o número de paginas da consulta,
	 * se {@link #page} > {@link #numberOfPages}.
	 */
	public boolean isOutOfRange() {
		return getPage() > getNumberOfPages();
	}
	/**
	 * Método de comodidade, que pode ser usado na view para informar a base 
	 * do link para gerar a páginação. Por padrão {@link #getBase()} retorna
	 * <tt>null</tt>, exceto se na lógica após uma consulta seja usado o método
	 * {@link #setBase(String)}.
	 * @return a base para o link de páginação
	 */
	public String getBase() {
		return base;
	}
	/**
	 * Método de comodidade, define a base para o link de paginação usada na view.
	 * @param base a base para o link de páginação
	 */
	public PagedList<T> setBase(String base) {
		this.base = base;
		return this;
	}
	
	/**
	 * Quantidade de páginas a serem exibidas no cabeçalho de paginação.
	 * @see #setDefaultMaxPages(int)
	 */
	public int getMaxPages() {
		return maxPages;
	}
	/**
	 * Altera a quantidade de páginas a serem exibidas no cabeçalho de paginação,
	 * esta alteração sera refletida apenas para esta uma instância especifica
	 * @param maxPages a nova quantidade
	 * @see #setDefaultMaxPages(int)
	 */
	public PagedList<T> setMaxPages(int maxPages) {
		this.maxPages = maxPages;
		return this;
	}
	
	/**
	 * Retorna a lista de páginas a serem exibidas no cabeçalho de paginação.
	 */
	public List<Page> getPages() {
		if (pages.isEmpty()) {
			synchronized (this) {
				if (pages.isEmpty()) {
					for (int i = getPagesStartAt(); i <= getPagesFinishAt(); i++) 
						pages.add(new Page(i));
				}
			}
		}
		return pages;
	}
	
	public class Page {
		private final int n;
		Page(int n) {
			this.n = n;
		}
		public int getNumber() {
			return n;
		}
		public boolean isActual() {
			return n == getPage();
		}
		@Override
		public String toString() {
			return String.valueOf(n);
		}
	}
	
	private int getNumberOfPagesLeft() {
		int qtd = (int)Math.ceil(getMaxPages()/2d)-1;
		return getPage() - qtd <= 0 ? getPage() - 1 : qtd;
	}
	
	private int getNumberOfPagesRight() {
		int qtd = getMaxPages() - (getNumberOfPagesLeft()+1);
		return getPage() + qtd > getNumberOfPages() ? getNumberOfPages() - getPage() : qtd;
	}
	
	/**
	 * Indica em que numero as paginas exibidas no cabelho iniciam.
	 */
	public int getPagesStartAt() {
		return getPage() - getNumberOfPagesLeft();
	}
	/**
	 * Indica em que numero as paginas exibidas no cabelho iniciam.
	 */
	public int getPagesFinishAt() {
		return getPage() + getNumberOfPagesRight();
	}
	
	/**
	 * Indica se ha' mais página(s) a esquerda, alem das exibidas.
	 */
	public boolean isMorePagesBefore() {
		return getPagesStartAt() > 1;
	}
	/**
	 * Indica se ha' mais página(s) a direita, alem das exibidas.
	 */
	public boolean isMorePagesAfter() {
		return getPagesFinishAt() < getNumberOfPages();
	}
	
	/**
	 * Insere este {@link PagedList} dentro de um adaptador em portuges, de forma 
	 * a facilitar o uso em uma view.
	 * @return uma {@link ListaPaginada} adaptada a partir deste {@link PagedList}.
	 * @see ListaPaginada#ListaPaginada(PagedList)
	 */
	public ListaPaginada<T> toVersaoEmPortugues() {
		return new ListaPaginada<T>(this);
	}
	
	/**
	 * @return a coluna que foi usada como Order 
	 */
	public String getSortCriterion() {
		return sortCriterion;
	}

	/**
	 * @return a direcao do sorting 
	 */
	public String getSortDirection() {
		return sortDirection;
	}
	
	
}
